﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Example.Mappers.Tests
{
    public static class CustomAssert
    {
        //checks that two instance have equivalent accessible property content (field contents are not checked)
        public static void AreEquivalent(object expected, object actual, params string[] fieldsToIgnore)
        {
            if (null != expected && null != actual)
            {
                var expectedType = expected.GetType();
                var expectedProperties = expectedType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .Where(p => p.CanRead && (null == fieldsToIgnore || !fieldsToIgnore.Contains(p.Name)));
                var actualType = actual.GetType();
                var actualProperties = actualType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .Where(p => p.CanRead && (null == fieldsToIgnore || !fieldsToIgnore.Contains(p.Name)));
                CollectionAssert.AreEquivalent(expectedProperties.Select(e => e.Name).ToArray(), actualProperties.Select(a => a.Name).ToArray());
                
                foreach (var properties in expectedProperties.Join(actualProperties, e => e.Name, a => a.Name, (e, a) => new { Expected = e, Actual = a }))
                {
                    var expectedPropertyValue = properties.Expected.GetValue(expected, null);
                    var actualPropertyValue = properties.Actual.GetValue(actual, null);
                    
                    if (expectedPropertyValue != null && actualPropertyValue != null)
                    {
                        decimal expectedPropertyAsDecimal, actualPropertyAsDecimal;

                        if (properties.Expected.PropertyType == properties.Actual.PropertyType)
                            //TODO? this won't work for cloned reference types
                            Assert.AreEqual(expectedPropertyValue, actualPropertyValue, "Property: '{0}.{1}'", expectedType.FullName, properties.Expected.PropertyType.Name);
                        
                        else if (tryCastToDecimal(expectedPropertyValue, out expectedPropertyAsDecimal) &&
                            tryCastToDecimal(actualPropertyValue, out actualPropertyAsDecimal))
                            //TODO this won't work if numeric value has been truncated
                            Assert.AreEqual(expectedPropertyAsDecimal, actualPropertyAsDecimal, "Property as decimal: '{0}.{1}'", expectedType.FullName, properties.Expected.PropertyType.Name);
                        
                        else if (typeof(IEnumerable).IsAssignableFrom(properties.Expected.PropertyType) &&
                            typeof(IEnumerable).IsAssignableFrom(properties.Actual.PropertyType))
                        {
                            var expectedPropertyValues = expectedPropertyValue as object[] ?? ((IEnumerable)expectedPropertyValue).Cast<object>().ToArray();
                            var actualPropertyValues = actualPropertyValue as object[] ?? ((IEnumerable)actualPropertyValue).Cast<object>().ToArray();
                            Assert.AreEqual(expectedPropertyValues.Count(), actualPropertyValues.Count());
                            
                            for (var i = 0; i < expectedPropertyValues.Count(); i++)
                            {
                                if (expectedPropertyValues[i].GetType() == actualPropertyValues[i].GetType())
                                    //TODO? this won't work for cloned reference types
                                    Assert.AreEqual(expectedPropertyValues[i], actualPropertyValues[i], "Property element: '{0}.{1}'", expectedType.FullName, properties.Expected.Name);
                                else
                                    AreEquivalent(expectedPropertyValues[i], actualPropertyValues[i], fieldsToIgnore);
                            }
                        }
                        else // non-enumerable container
                            AreEquivalent(expectedPropertyValue, actualPropertyValue, fieldsToIgnore);
                    }
                    else
                        Assert.AreEqual(expectedPropertyValue, actualPropertyValue, "Property: '{0}.{1}'", expectedType.FullName, properties.Expected.Name);
                }
            }
            else
                Assert.AreEqual(expected, actual);
        }
        private static bool tryCastToDecimal(object value, out decimal decimalValue)
        {
            try
            {
                decimalValue = Convert.ToDecimal(value);
                return true;
            }
            catch (InvalidCastException)
            {
                decimalValue = 0;
                return false;
            }
        }
    }
}
